/**
 * Copyright (c) Huawei Technologies Co., Ltd. 2019-2021. All rights reserved.
 */

#include <gtest/gtest.h>

#include "plugins/ecmascript/runtime/ecma_vm.h"
#include "plugins/ecmascript/runtime/js_array.h"
#include "plugins/ecmascript/runtime/js_handle.h"
#include "plugins/ecmascript/runtime/object_factory.h"
#include "runtime/include/runtime.h"
#include "runtime/include/panda_vm.h"
#include "runtime/include/thread_scopes.h"
#include "runtime/mem/object_helpers-inl.h"

namespace ark::ecmascript {

inline std::string Separator()
{
#ifdef _WIN32
    return "\\";
#else
    return "/";
#endif
}

class DynamicObjectHelpersTest : public testing::Test {
public:
    NO_COPY_SEMANTIC(DynamicObjectHelpersTest);
    NO_MOVE_SEMANTIC(DynamicObjectHelpersTest);

    DynamicObjectHelpersTest()
    {
        RuntimeOptions options;
        options.SetLoadRuntimes({"ecmascript"});
        options.SetGcType("epsilon");
        options.SetGcTriggerType("debug-never");
        auto execPath = ark::os::file::File::GetExecutablePath();
        std::string pandaStdLib =
            execPath.Value() + Separator() + ".." + Separator() + "pandastdlib" + Separator() + "pandastdlib.bin";
        options.SetBootPandaFiles({pandaStdLib});

        Runtime::Create(options);

        vm_ = EcmaVM::Cast(Runtime::GetCurrent()->GetPandaVM());
        thread_ = vm_->GetAssociatedJSThread();
        factory_ = vm_->GetFactory();
        handleScope_ = new EcmaHandleScope(thread_);
        vm_->GetMutatorLock()->ReadLock();
        initialDynClass_ = factory_->NewEcmaDynClassClass(nullptr, JSHClass::SIZE, JSType::HCLASS);
        JSHClass *dynclass = reinterpret_cast<JSHClass *>(initialDynClass_.GetTaggedValue().GetTaggedObject());
        dynclass->SetClass(dynclass);
        dynclass->GetHClass()->SetNativeFieldMask(JSHClass::NATIVE_FIELDS_MASK);
    }

    ~DynamicObjectHelpersTest() override
    {
        vm_->GetMutatorLock()->Unlock();
        delete handleScope_;
        Runtime::Destroy();
    }

    JSHandle<JSHClass> NewDynClass()
    {
        return factory_->CreateDynClass<JSHClass>(*initialDynClass_, JSType::HCLASS, HClass::HCLASS);
    }

protected:
    // NOLINTNEXTLINE(misc-non-private-member-variables-in-classes)
    JSThread *thread_ {};
    // NOLINTNEXTLINE(misc-non-private-member-variables-in-classes)
    ObjectFactory *factory_ {};

private:
    EcmaHandleScope *handleScope_ {};
    EcmaVM *vm_ {};
    JSHandle<JSHClass> initialDynClass_;
};

TEST_F(DynamicObjectHelpersTest, TestDynClass)
{
    JSHandle<JSHClass> dynClass = NewDynClass();
    JSHClass *hclass = *dynClass;

    JSHandle<JSObject> proto = factory_->NewEmptyJSObject();
    hclass->SetProto(thread_, proto);
    JSHandle<JSObject> attributes = factory_->NewEmptyJSObject();
    hclass->SetLayout(thread_, attributes);
    JSHandle<JSObject> transitions = factory_->NewEmptyJSObject();
    hclass->SetTransitions(thread_, transitions);
    JSHandle<JSObject> parent = factory_->NewEmptyJSObject();
    hclass->SetParent(thread_, parent);
    JSHandle<JSObject> protoChangedCell = factory_->NewEmptyJSObject();
    hclass->SetProtoChangeMarker(thread_, protoChangedCell);
    JSHandle<JSObject> protoChangeDetails = factory_->NewEmptyJSObject();
    hclass->SetProtoChangeDetails(thread_, protoChangeDetails);
    JSHandle<JSObject> enumCache = factory_->NewEmptyJSObject();
    hclass->SetEnumCache(thread_, enumCache);

    PandaVector<std::pair<ObjectHeader *, size_t>> objectsSeen = {
        {proto.GetObject<ObjectHeader>(), JSHClass::GetProtoOffset()},
        {attributes.GetObject<ObjectHeader>(), JSHClass::GetLayoutOffset()},
        {transitions.GetObject<ObjectHeader>(), JSHClass::GetTransitionsOffset()},
        {parent.GetObject<ObjectHeader>(), JSHClass::GetParentOffset()},
        {protoChangedCell.GetObject<ObjectHeader>(), JSHClass::GetProtoChangeMarkerOffset()},
        {protoChangeDetails.GetObject<ObjectHeader>(), JSHClass::GetProtoChangeDetailsOffset()},
        {enumCache.GetObject<ObjectHeader>(), JSHClass::GetEnumCacheOffset()}};
    auto handler = [&objectsSeen]([[maybe_unused]] ObjectHeader *obj, ObjectHeader *field, uint32_t offset,
                                  [[maybe_unused]] bool isVolatile) {
        auto it =
            std::find_if(objectsSeen.begin(), objectsSeen.end(),
                         [field](const std::pair<ObjectHeader *, size_t> &entry) { return entry.first == field; });
        EXPECT_NE(objectsSeen.end(), it);
        if (it != objectsSeen.end()) {
            EXPECT_NE(nullptr, it->first);
            EXPECT_EQ(it->second, offset);
            it->first = nullptr;
        }
        return true;
    };
    mem::GCDynamicObjectHelpers::TraverseAllObjectsWithInfo<false>(dynClass.GetObject<ObjectHeader>(), handler);
    size_t count = 0;
    for (auto entry : objectsSeen) {
        ASSERT_EQ(nullptr, entry.first) << "Object " << count << " was not seen";
        ++count;
    }
}

TEST_F(DynamicObjectHelpersTest, TestDynObject)
{
    JSHandle<JSObject> object = factory_->NewEmptyJSObject();
    JSHandle<EcmaString> key = factory_->NewFromStdString("key");
    JSHandle<JSObject> value = factory_->NewEmptyJSObject();
    JSObject::SetProperty(thread_, object, JSHandle<JSTaggedValue>(thread_, key.GetTaggedValue()),
                          JSHandle<JSTaggedValue>(thread_, value.GetTaggedValue()));

    PandaQueue<ObjectHeader *> queue;
    bool valueSeen = false;
    auto handler = [&value, &valueSeen, &queue]([[maybe_unused]] ObjectHeader *obj, ObjectHeader *field,
                                                [[maybe_unused]] uint32_t offset, [[maybe_unused]] bool isVolatile) {
        if (field == value.GetObject<ObjectHeader>()) {
            valueSeen = true;
        } else {
            queue.push(field);
        }
        return true;
    };
    queue.push(object.GetObject<ObjectHeader>());
    while (!queue.empty()) {
        ObjectHeader *front = queue.front();
        queue.pop();
        mem::GCDynamicObjectHelpers::TraverseAllObjectsWithInfo<false>(front, handler);
    }
    ASSERT_TRUE(valueSeen);
}

TEST_F(DynamicObjectHelpersTest, TestDynArray)
{
    JSHandle<JSArray> object = factory_->NewJSArray();
    JSHandle<JSObject> value = factory_->NewEmptyJSObject();
    JSArray::FastSetPropertyByValue(thread_, JSHandle<JSTaggedValue>(thread_, object.GetTaggedValue()), 0,
                                    JSHandle<JSTaggedValue>(thread_, value.GetTaggedValue()));

    PandaQueue<ObjectHeader *> queue;
    bool valueSeen = false;
    auto handler = [&value, &valueSeen, &queue]([[maybe_unused]] ObjectHeader *obj, ObjectHeader *field,
                                                [[maybe_unused]] uint32_t offset, [[maybe_unused]] bool isVolatile) {
        if (field == value.GetObject<ObjectHeader>()) {
            valueSeen = true;
        } else {
            queue.push(field);
        }
        return true;
    };
    queue.push(object.GetObject<ObjectHeader>());
    while (!queue.empty()) {
        ObjectHeader *front = queue.front();
        queue.pop();
        mem::GCDynamicObjectHelpers::TraverseAllObjectsWithInfo<false>(front, handler);
    }
    ASSERT_TRUE(valueSeen);
}

}  // namespace ark::ecmascript
